﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.BusinessData.Runtime;
using System.Collections;
using Microsoft.BusinessData.Infrastructure;
using Microsoft.BusinessData.MetadataModel;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Net;
using System.Xml;
using Microsoft.BusinessData.MetadataModel.Collections;

namespace BdcRestConnector
{
    public class RestConnector : ISystemUtility
    {
        public IEnumerator CreateEntityInstanceDataEnumerator(object rawStream, ISharedEntityState sharedEntityState)
        {
            var enumerator = rawStream as IEnumerator;
            if (enumerator != null)
            {
                return enumerator;
            }
            var enumerable = rawStream as IEnumerable;
            if (enumerable != null)
            {
                return enumerable.GetEnumerator();
            }
            throw new InvalidOperationException();
        }

        public IConnectionManager DefaultConnectionManager
        {
            get { return null; }
        }

        public ITypeReflector DefaultTypeReflector
        {
            get { return null; }
        }

        public void ExecuteStatic(IMethodInstance mi, ILobSystemInstance si, object[] args, IExecutionContext context)
        {
            if (mi == null)
            {
                throw new ArgumentNullException("methodInstance");
            }
            if (si == null)
            {
                throw new ArgumentNullException("lobSystemInstance");
            }
            if (args == null)
            {
                throw new ArgumentNullException("args");
            }

            INamedPropertyDictionary systemProperties = si.GetProperties();
            object inputServiceUrl = null;
            if (!systemProperties.TryGetValue("ServiceUrl", out inputServiceUrl))
                throw new InvalidOperationException("Cannot find the base service URL.");

            INamedPropertyDictionary methodProperties = mi.GetProperties();
            object inputMethodUrl = null;
            if (!methodProperties.TryGetValue("Url", out inputMethodUrl))
                throw new InvalidOperationException("Cannot find the URL property of the method");

            string serviceUrl = String.Format("{0}{1}", inputServiceUrl, inputMethodUrl);
            if (String.IsNullOrEmpty(serviceUrl))
                throw new InvalidOperationException("The specified URL cannot be null");

            IParameterCollection methodParams = mi.GetMethod().GetParameters();
            switch (mi.MethodInstanceType)
            {
                case MethodInstanceType.Finder:
                    if (!mi.HasReturnTypeDescriptor)
                        throw new InvalidOperationException("Finder operation must have a return parameter with a type descriptor");

                    // the last parameter is always the RETURN one
                    args[args.Length - 1] = GetEntityCollection(serviceUrl, mi);
                    break;

                case MethodInstanceType.SpecificFinder:
                    if (!mi.HasReturnTypeDescriptor)
                        throw new InvalidOperationException("Finder operation must have a return parameter with a type descriptor");

                    if (args.Length <= 1)
                        throw new InvalidOperationException("Specific finder must have at least two parameters - one in and one return");

                    for (int i = 0; i < methodParams.Count; i++)
                    {
                        if (methodParams[i].Direction == DirectionType.In)
                        {
                            serviceUrl = serviceUrl.Replace(String.Format("@{0}", methodParams[i].Name), args[i].ToString());
                        }
                    }

                    // the last parameter is always the RETURN one
                    args[args.Length - 1] = GetEntity(serviceUrl, mi);
                    break;
            }
        }

        private static XElement GetRestResult(string url)
        {
            XDocument doc = null;
            try
            {
                doc = XDocument.Load(url);
            }
            finally
            {
                //~
            }
            if (doc != null) return doc.Root;

            throw new Exception("Cannot connect to the service");
        }

        private static object GetEntityCollection(string serviceUrl, IMethodInstance methodInstance)
        {
            XElement rootElement = GetRestResult(serviceUrl);
            return MapEntityCollection(rootElement, methodInstance.GetReturnTypeDescriptor(), methodInstance.GetProperties());
        }

        private static object GetEntity(string serviceUrl, IMethodInstance methodInstance)
        {
            XElement rootElement = GetRestResult(serviceUrl);
            ITypeDescriptor entityTypeDescriptor = methodInstance.GetReturnTypeDescriptor();
            Type entityType = Type.GetType(entityTypeDescriptor.TypeName);

            INamedPropertyDictionary properties = methodInstance.GetProperties();
            XmlNamespaceManager xmlNsResolver = null;
            if (properties.ContainsKey("Namespaces"))
            {
                string namespaces = properties["Namespaces"].ToString();
                xmlNsResolver = GetNamespaceManagaer(namespaces);
            }
            return MapEntity(rootElement, xmlNsResolver, entityTypeDescriptor, entityType);
        }

        private static object MapEntityCollection(XElement rootElement,
            ITypeDescriptor collectionTypeDescriptor, INamedPropertyDictionary methodProperties)
        {
            if (!methodProperties.ContainsKey("ElementsExpression"))
                throw new InvalidMetadataPropertyException("Cannot find ElementsExpression property of the method");

            string elementsExpression = methodProperties["ElementsExpression"].ToString();
            ITypeDescriptorCollection childTypeDescriptors = collectionTypeDescriptor.GetChildTypeDescriptors();
            if (childTypeDescriptors == null || childTypeDescriptors.Count == 0)
                throw new InvalidOperationException("The return parameter of a Finder method must contain a collection type descriptor and an entity type descriptor as its only child!");

            ITypeDescriptor entityTypeDescriptor = childTypeDescriptors[0];
            Type entityType = Type.GetType(entityTypeDescriptor.TypeName);
            Type resultCollectionType = typeof(List<>).MakeGenericType(entityType);
            XmlNamespaceManager xmlNsResolver = null;
            if (methodProperties.ContainsKey("Namespaces"))
            {
                string namespaces = methodProperties["Namespaces"].ToString();
                xmlNsResolver = GetNamespaceManagaer(namespaces);
            }

            IEnumerable<XElement> elements = rootElement.XPathSelectElements(elementsExpression, xmlNsResolver);
            IList resultList = Activator.CreateInstance(resultCollectionType) as IList;
            foreach (XElement element in elements)
            {
                object entityInstance = MapEntity(element, xmlNsResolver, entityTypeDescriptor, entityType);
                resultList.Add(entityInstance);
            }
            return resultList;
        }

        private static object MapEntity(XElement element, IXmlNamespaceResolver nsResolver,
            ITypeDescriptor entityTypeDescriptor, Type entityType)
        {
            object entityInstance = Activator.CreateInstance(entityType);

            foreach (ITypeDescriptor propertyTypeDescriptor in entityTypeDescriptor.GetChildTypeDescriptors())
            {
                string expression = propertyTypeDescriptor.GetProperties()["Expression"] as string;

                IEnumerable propertyExpressionResult = element.XPathEvaluate(expression, nsResolver) as IEnumerable;
                object propertyXmlValue = null;
                if (propertyExpressionResult != null)
                {
                    foreach (object xmlObject in propertyExpressionResult)
                    {
                        var xmlElement = xmlObject as XElement;
                        if (xmlElement != null)
                        {
                            propertyXmlValue = xmlElement.Value;
                            break;
                        }
                        var xmlAttribute = xmlObject as XAttribute;
                        if (xmlAttribute != null)
                        {
                            propertyXmlValue = xmlAttribute.Value;
                            break;
                        }
                    }
                }

                object propertyValue = ParsePropertyValue(propertyTypeDescriptor, propertyXmlValue);
                if (propertyValue != null)
                {
                    entityType.GetProperty(propertyTypeDescriptor.Name)
                        .SetValue(entityInstance, propertyValue, null);
                }
            }
            return entityInstance;
        }

        private static object ParsePropertyValue(ITypeDescriptor typeDescriptor, object input)
        {
            if (typeDescriptor == null)
                throw new ArgumentNullException("typeDescriptor");

            if (input == null) return null;

            switch (typeDescriptor.TypeName)
            {
                case "System.String":
                    return input as string;

                case "System.Int32":
                    int result;
                    if (Int32.TryParse(input as string, out result))
                    {
                        return result;
                    }
                    return null;

                case "System.DateTime":
                    DateTime result2;
                    if (DateTime.TryParse(input as string, out result2))
                    {
                        return result2;
                    }
                    return null;
            }
            throw new Exception("Unrecognized property type");
        }

        private static XmlNamespaceManager GetNamespaceManagaer(string input)
        {
            XmlNamespaceManager mng = new XmlNamespaceManager(new NameTable());

            if (!String.IsNullOrEmpty(input))
            {
                string[] namespaces = input.Split(new char[] { ' ' });
                foreach (string nsPart in namespaces)
                {
                    string[] data = nsPart.Split(new char[] { '|' });
                    mng.AddNamespace(data[0], data[1]);
                }
            }

            return mng;
        }

    }
}
